
package com.phm.phmneuron.gng;

import com.phm.phmneuron.DistanceMethod;
import com.phm.phmneuron.EuclideanDistanceMethod;
import com.phm.phmneuron.NeuralNetwork;
import com.phm.phmneuron.ParametersContainer;
import com.phm.phmneuron.ProcessOnParameter;
import com.phm.phmneuron.connection.Connection;
import com.phm.phmneuron.neuron.Neuron;
import com.phm.phmneuron.neuron.NeuronsContainer;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

/**
 *
 * @author PARHAM
 */

public class GNGNeuralNetwork extends NeuralNetwork {

    public static final int EPSILON_B_KEY = 0x0011;
    public static final int EPSILON_N_KEY = 0x0012;
    // needed age for an edge to be removed
    public static final int MAX_EDGE_AGE_KEY = 0x0013;
    // number signal generations needed to insert new node
    public static final int LANDA_KEY = 0x0014;
    public static final int ERRORRATE_A_KEY = 0x0015;
    public static final int ERRORRATE_B_KEY = 0x0016;
    
    
    protected DistanceMethod _distanceMethod = new EuclideanDistanceMethod();

    public GNGNeuralNetwork () {
        parameters.updateParameter(EPSILON_B_KEY, 0.05);
        parameters.updateParameter(EPSILON_N_KEY, 0.0006);
        parameters.updateParameter(MAX_EDGE_AGE_KEY, 88);
        parameters.updateParameter(LANDA_KEY, 200);
        parameters.updateParameter(ERRORRATE_A_KEY, 0.5);
        parameters.updateParameter(ERRORRATE_B_KEY, 0.0005);
    }
    
    public void setDistanceMethod (DistanceMethod dm) {
        _distanceMethod = Objects.requireNonNull(dm);
    }
    public DistanceMethod getDistanceMethod () {
        return _distanceMethod;
    }
    
    private GNGNeuron [] findTwoNearest () {
        GNGNeuron [] ns = (GNGNeuron []) neurons.getNeurons();
        Arrays.sort(ns);
        GNGNeuron [] result = new GNGNeuron [2];
        result [0] = ns [0];
        result [1] = ns [1];
        return result;
    }
    private void calculateDistances (NeuronsContainer neurons, float [] signal) {
        Neuron [] ns = neurons.getNeurons();
        for (int index = 0; index < ns.length; index++) {
            GNGNeuron temp = (GNGNeuron) ns [index];
            float dis = _distanceMethod.distance(temp.central, signal);
            float [] disDim = _distanceMethod.distanceDimension(temp.central, signal);
            temp.parameters.updateParameter(GNGNeuron.NEURON_DISTANCE_KEY, dis);
            temp.parameters.updateParameter(GNGNeuron.NEURON_DIS_DIMENSION_KEY, disDim);
        }
    }
    private void removeConnectionsWithHighAge () {
        GNGNeuron [] nodes = (GNGNeuron []) neurons.getNeurons();
        for (int index = 0; index < nodes.length; index++) {
            Connection [] nl = nodes[index].connections.toArray(new Connection [0]);
            for (int dindex = 0; dindex < nl.length; dindex++) {
                removeConnection(nl[index]);
            }
        }
    }
    private void removeNeuronWithoutConnection () {
        Neuron [] ns = neurons.getNeurons ();
        for (int index = 0; index < neurons.countNeurons(); index++) {
            if (ns[index].connections.size() < 1) {
                removeNeuron(ns[index]);
            }
        }
    }

    @Override
    protected boolean learnMethod (List<float[]> signals) {
        if (signals.size() < 3) return false;
        // GET NECESSARY PARAMETERS
        float _epsilonB = (Float) parameters.getParameter(EPSILON_B_KEY);
        float _epsilonN = (Float) parameters.getParameter(EPSILON_N_KEY);
        int _edgeMaxAge = (Integer) parameters.getParameter(MAX_EDGE_AGE_KEY);
        int _landa = (Integer) parameters.getParameter(LANDA_KEY);
        float _aErrorRate = (Float) parameters.getParameter(ERRORRATE_A_KEY);
        float _bErrorRate = (Float) parameters.getParameter(ERRORRATE_B_KEY);
        ///////////////////
        addNeuron(new GNGNeuron(signals.remove(0)));
        addNeuron(new GNGNeuron(signals.remove(0)));
        float [] buffer = null;
        for (int sindex = 0; sindex < signals.size(); sindex++) {
            if (isRestrictionsFulfiled()) {
                break;
            }
            buffer = signals.get(sindex);
            // Finding Distance
            calculateDistances(neurons, buffer);
            // Find out two best neurons
            GNGNeuron [] twobest = findTwoNearest();
            ////////////////////////////////////////////////////////
            // Create Connection between two nearest neurons
            Connection c = new Connection(twobest[0], twobest[1], 0);
            updateConnection(c);
            ////////////////////////////////////////////////////////
            // Add squared error of winner unit to a local error
            twobest[0].parameters.processOnParameter(GNGNeuron.NEURON_DISTANCE_KEY, new CalculateWinnerError());
            ////////////////////////////////////////////////////////
            // Update neuron weights
            GNGNeuron [] neighbors = twobest[0].connections.getNeighbors().toArray(new GNGNeuron [0]);
            for (int index = 0; index < neighbors.length; index++) {
                // Update winner neuron's neighbors' weights
                float [] dimDist = (float []) neighbors[index].parameters.getParameter(GNGNeuron.NEURON_DIS_DIMENSION_KEY);
                for (int dim = 0; dim < buffer.length; dim++) {
                    neighbors[index].central [dim] += (dimDist [index] * _epsilonN);
                }
            }
            // Update winner neuron's weight
            float [] dimDist = (float []) twobest[0].parameters.getParameter(GNGNeuron.NEURON_DIS_DIMENSION_KEY);
            for (int dim = 0; dim < buffer.length; dim++) {
                twobest[0].central[dim] += (dimDist [dim] * _epsilonB);
            }
            ////////////////////////////////////////////////////////
            // Increase winner neuron's connection's age
            twobest[0].connections.incerementConnectionsEdgeValue();
            ////////////////////////////////////////////////////////
            // Remove neuron's connections which their ages are more than maximum
            removeConnectionsWithHighAge();
            ////////////////////////////////////////////////////////
            // Remove neurons with no connections
            removeNeuronWithoutConnection();
            ////////////////////////////////////////////////////////
            // Insert new neuron
            if ((sindex + 1) % _landa == 0) {
                // Find the neural unit with highest accumulated error
                Neuron [] ns = neurons.getNeurons();
                GNGNeuron highestLE = (GNGNeuron) ns [0];
                for (int index = 1; index < ns.length; index++) {
                    float hn = (Float) highestLE.parameters.getParameter(GNGNeuron.NEURON_LOCALERROR_KEY);
                    float tempn = (Float) ns[index].parameters.getParameter(GNGNeuron.NEURON_LOCALERROR_KEY);
                    if (hn < tempn) {
                        highestLE = (GNGNeuron) ns [index];
                    }
                }
                // Find the neighbor of q which has the highest accumulated error
                neighbors = highestLE.connections.getNeighbors().toArray(new GNGNeuron [0]);
                GNGNeuron hMaxLENEighbor = neighbors [0];
                for (int index = 1; index < neighbors.length; index++) {
                    GNGNeuron temp2 = neighbors [index];
                    float maxle = (Float) hMaxLENEighbor.parameters.getParameter(index);
                    float tle = (Float) temp2.parameters.getParameter(GNGNeuron.NEURON_LOCALERROR_KEY);
                    if (tle > maxle) {
                        hMaxLENEighbor = temp2;
                    }
                }
                // Put new neuron in to the system
                float [] newWeight = new float [highestLE.dimension];
                for (int index = 0; index < newWeight.length; index++ ) {
                    newWeight [index] = (highestLE.central[index] + hMaxLENEighbor.central[index]) / 2;
                }
                GNGNeuron newNeuron = new GNGNeuron (newWeight);
                addNeuron(newNeuron);
                // Remove Edges between the selected neurons
                removeConnection(new Connection(highestLE, hMaxLENEighbor));
                // Add edge between new node and f and q
                updateConnection(new Connection(newNeuron, highestLE, 0));
                updateConnection(new Connection(newNeuron, hMaxLENEighbor, 0));
                // Prepare error vector for new node
                // Decrease Error value of neuron f and q
                highestLE.parameters.processOnParameter(GNGNeuron.NEURON_LOCALERROR_KEY, new UpdateErrorNeuron(_aErrorRate));
                hMaxLENEighbor.parameters.processOnParameter(GNGNeuron.NEURON_LOCALERROR_KEY, new UpdateErrorNeuron(_aErrorRate));
                // Calculate Error for new node
                float errorHighest = (Float) highestLE.parameters.getParameter(GNGNeuron.NEURON_LOCALERROR_KEY);
                float errorMax = (Float) hMaxLENEighbor.parameters.getParameter(GNGNeuron.NEURON_LOCALERROR_KEY);
                newNeuron.parameters.updateParameter(GNGNeuron.NEURON_LOCALERROR_KEY, (errorHighest + errorMax) / 2);
            }
            // Decrease error of all units
            Neuron [] nodes = neurons.getNeurons();
            for (int index = 0; index < nodes.length; index++) {
                nodes [index].parameters.processOnParameter(GNGNeuron.NEURON_LOCALERROR_KEY, new UpdateErrorNeuron(_bErrorRate));
            }
            eventNetworkUpdated();
        }
        return true;
    }

    @Override
    public boolean learn (float[] signal) {
        if (isRestrictionsFulfiled()) {
            return true;
        }
        // GET NECESSARY PARAMETERS
        float _epsilonB = (Float) parameters.getParameter(EPSILON_B_KEY);
        float _epsilonN = (Float) parameters.getParameter(EPSILON_N_KEY);
        int _edgeMaxAge = (Integer) parameters.getParameter(MAX_EDGE_AGE_KEY);
        int _landa = (Integer) parameters.getParameter(LANDA_KEY);
        float _aErrorRate = (Float) parameters.getParameter(ERRORRATE_A_KEY);
        float _bErrorRate = (Float) parameters.getParameter(ERRORRATE_B_KEY);
        ///////////////////
        if (neurons.countNeurons() < 2) {
            addNeuron(new GNGNeuron(signal));
            return true;
        }
        //////////////////
        float [] buffer = signal;
        // Finding Distance
        calculateDistances(neurons, buffer);
        // Find out two best neurons
        GNGNeuron [] twobest = findTwoNearest();
        ////////////////////////////////////////////////////////
        // Create Connection between two nearest neurons
        Connection c = new Connection(twobest[0], twobest[1], 0);
        updateConnection(c);
        ////////////////////////////////////////////////////////
        // Add squared error of winner unit to a local error
        twobest[0].parameters.processOnParameter(GNGNeuron.NEURON_DISTANCE_KEY, new CalculateWinnerError());
        ////////////////////////////////////////////////////////
        // Update neuron weights
        GNGNeuron [] neighbors = twobest[0].connections.getNeighbors().toArray(new GNGNeuron [0]);
        for (int index = 0; index < neighbors.length; index++) {
            // Update winner neuron's neighbors' weights
            float [] dimDist = (float []) neighbors[index].parameters.getParameter(GNGNeuron.NEURON_DIS_DIMENSION_KEY);
            for (int dim = 0; dim < buffer.length; dim++) {
                neighbors[index].central [dim] += (dimDist [index] * _epsilonN);
            }
        }
        // Update winner neuron's weight
        float [] dimDist = (float []) twobest[0].parameters.getParameter(GNGNeuron.NEURON_DIS_DIMENSION_KEY);
        for (int dim = 0; dim < buffer.length; dim++) {
            twobest[0].central[dim] += (dimDist [dim] * _epsilonB);
        }
        ////////////////////////////////////////////////////////
        // Increase winner neuron's connection's age
        twobest[0].connections.incerementConnectionsEdgeValue();
        ////////////////////////////////////////////////////////
        // Remove neuron's connections which their ages are more than maximum
        removeConnectionsWithHighAge();
        ////////////////////////////////////////////////////////
        // Remove neurons with no connections
        removeNeuronWithoutConnection();
        ////////////////////////////////////////////////////////
        // Insert new neuron
        int sindex = (Integer) parameters.getParameter(NUM_FEED_SIGNALS);
        if ((sindex + 1) % _landa == 0) {
            // Find the neural unit with highest accumulated error
            Neuron [] ns = neurons.getNeurons();
            GNGNeuron highestLE = (GNGNeuron) ns [0];
            for (int index = 1; index < ns.length; index++) {
                float hn = (Float) highestLE.parameters.getParameter(GNGNeuron.NEURON_LOCALERROR_KEY);
                float tempn = (Float) ns[index].parameters.getParameter(GNGNeuron.NEURON_LOCALERROR_KEY);
                if (hn < tempn) {
                    highestLE = (GNGNeuron) ns [index];
                }
            }
            // Find the neighbor of q which has the highest accumulated error
            neighbors = highestLE.connections.getNeighbors().toArray(new GNGNeuron [0]);
            GNGNeuron hMaxLENEighbor = neighbors [0];
            for (int index = 1; index < neighbors.length; index++) {
                GNGNeuron temp2 = neighbors [index];
                float maxle = (Float) hMaxLENEighbor.parameters.getParameter(index);
                float tle = (Float) temp2.parameters.getParameter(GNGNeuron.NEURON_LOCALERROR_KEY);
                if (tle > maxle) {
                    hMaxLENEighbor = temp2;
                }
            }
            // Put new neuron in to the system
            float [] newWeight = new float [highestLE.dimension];
            for (int index = 0; index < newWeight.length; index++ ) {
                newWeight [index] = (highestLE.central[index] + hMaxLENEighbor.central[index]) / 2;
            }
            GNGNeuron newNeuron = new GNGNeuron (newWeight);
            addNeuron(newNeuron);
            // Remove Edges between the selected neurons
            removeConnection(new Connection(highestLE, hMaxLENEighbor));
            // Add edge between new node and f and q
            updateConnection(new Connection(newNeuron, highestLE, 0));
            updateConnection(new Connection(newNeuron, hMaxLENEighbor, 0));
            // Prepare error vector for new node
            // Decrease Error value of neuron f and q
            highestLE.parameters.processOnParameter(GNGNeuron.NEURON_LOCALERROR_KEY, new UpdateErrorNeuron(_aErrorRate));
            hMaxLENEighbor.parameters.processOnParameter(GNGNeuron.NEURON_LOCALERROR_KEY, new UpdateErrorNeuron(_aErrorRate));
            // Calculate Error for new node
            float errorHighest = (Float) highestLE.parameters.getParameter(GNGNeuron.NEURON_LOCALERROR_KEY);
            float errorMax = (Float) hMaxLENEighbor.parameters.getParameter(GNGNeuron.NEURON_LOCALERROR_KEY);
            newNeuron.parameters.updateParameter(GNGNeuron.NEURON_LOCALERROR_KEY, (errorHighest + errorMax) / 2);
        }
        // Decrease error of all units
        Neuron [] nodes = neurons.getNeurons();
        for (int index = 0; index < nodes.length; index++) {
            nodes [index].parameters.processOnParameter(GNGNeuron.NEURON_LOCALERROR_KEY, new UpdateErrorNeuron(_bErrorRate));
        }
        parameters.updateParameter(NUM_FEED_SIGNALS, new IncreaseQuantity());
        eventNetworkUpdated();
        return true;
    }
    
    @Override
    public Neuron result(float [] signal) {
        throw new UnsupportedOperationException("Not supported yet.");
    }
    
    protected class CalculateWinnerError extends ProcessOnParameter {
        @Override
        public Object process(Object data, ParametersContainer c) {
            float dis = (Float) c.getParameter(GNGNeuron.NEURON_DISTANCE_KEY);
            float lerror = (Float) data;
            return lerror + (dis * dis);
        }
    }
    protected class UpdateErrorNeuron extends ProcessOnParameter {
        float factor = 0;
        public UpdateErrorNeuron (float f) {
            factor = f;
        }
        @Override
        public Object process(Object data, ParametersContainer c) {
            float error = (Float) data;
            return error - (error * factor);
        }
        
    }
}
